今回は3Dメッシュのアニメーションと、ボーンを用いたアニメーション(スキニングやスケルタルアニメーションと呼ばれる)の実装方法についてみていく。3Dメッシュのアニメーションについては、ボーンを用いたメッシュのアニメーションを実現する基礎技術となる。なお、JavaFXにおけるアニメーションの基礎は『
JavaFX アニメーション』を参照のこと。
■ メッシュのアニメーション
JavaFXにおけるメッシュは、Meshインスタンスに対して新しい頂点座標・テクスチャ座標・面の設定を単純に再登録することで実現できる。これは設定用関数であるgetPoints関数・getTexCoods関数・getFaces関数の戻り値の型がObservableFloatArray(リスナーが発生時の変更を追跡できるfloat[]配列)であるためであり、再登録を感知したMeshクラス内部のリスナーが描画内容を変更するためである。
■ メッシュ・アニメーションのサンプルコード
以下にメッシュをアニメーション(動的変形)させるサンプルコードを示す。サンプルでは0.5秒毎にメッシュの大きさが変更される。
◇サンプルコード
package application_fx;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.collections.ObservableFloatArray;
import javafx.scene.Group;
import javafx.scene.LightBase;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.stage.Stage;
public class TestTriangleMeshAnimation extends Application {
public static void main(String[] args)
{
launch( args );
}
@Override
public void start(Stage primaryStage) throws Exception
{
// シーングラフの構成
Group root = new Group();
// モデルデータの取り込み
root.getChildren().add( createTriangleMesh() );
// シーンの作成
// 3Dシーンの奥行きを表現するため、Zバッファを有効にする
Scene scene = new Scene( root , 1000 , 750 , true );
scene.setFill( Color.BLACK );
// カメラ設定
PerspectiveCamera camera = new PerspectiveCamera( true );
camera.setFarClip( 300 );
camera.setTranslateZ( -50 );
scene.setCamera( camera );
// 光源設定
LightBase light = new PointLight();
light.setTranslateZ( -50 );
root.getChildren().add( light );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
/**
* トライアングル・メッシュを作成
*
* 【メッシュ】
* p0┏━┓p3
* ┃\┃
* p1┗━┛p2
*
* @return
*/
public Node createTriangleMesh()
{
// メッシュ
MeshView meshView = new MeshView();
TriangleMesh mesh = new TriangleMesh();
// メッシュを作成
float[] points = { -5 ,-5 ,0 , // p0
-5 ,5 ,0 , // p1
5 ,5 ,0 , // p2
5 ,-5 ,0 }; // p3
float[] texCoords = { 0 , 0 };
int[] faces = { 0 , 0 , 1 , 0 , 2 , 0,
2 , 0 , 3 , 0 , 0 , 0 };
mesh.getPoints().addAll( points );
mesh.getTexCoords().addAll( texCoords );
mesh.getFaces().addAll( faces );
// メッシュを登録
meshView.setMesh( mesh );
// メッシュアニメーション開始
new MeshAnimation( meshView ).start();
return meshView;
}
/**
* アニメーションタイマー・クラス
*/
private class MeshAnimation extends AnimationTimer
{
// アニメーション対象ノード
private TriangleMesh mesh = null;
// アニメーション間隔(nano sec)
private long duration = 500 * 1000000L; // 500ミリ秒
private long startTime = 0;
private long beforeCount = -1;
public MeshAnimation( MeshView meshView )
{
// 内部変数の初期化
this.mesh = (TriangleMesh) meshView.getMesh();
}
@Override
public void handle( long now )
{
// アニメーションの開始時間を取得
if( startTime == 0 ){ startTime = now; }
// アニメーションカウントを計算
// カウントが進んでいない場合は、処理を終了
Long count = ( now - startTime ) / duration;
if( beforeCount != count ){ beforeCount = count; }
else{ return; }
// メッシュの大きさを計算
float size = (float) ( count % 5 ) + 1.0f ;
// メッシュの形を変更
ObservableFloatArray oldPoints = mesh.getPoints();
float[] newPoints = new float[ oldPoints.size() ];
for( int i=0 ; i<oldPoints.size() ; i++ )
{
// 新しい配列を作成
float point = oldPoints.get( i );
if( point < 0.0f ){ newPoints[i] = -size; }
if( point > 0.0f ){ newPoints[i] = size; }
if( point == 0.0f ){ newPoints[i] = 0; }
// 配列の内容を標準出力
System.out.print( "," + newPoints[i] );
}
// 新しいメッシュを設定
mesh.getPoints().setAll( newPoints );
// 現在のアニメーションカウントを標準出力
System.out.println( " count:" + count );
}
}
}
◇ 実行結果
標準出力
,-1.0,-1.0,0.0,-1.0,1.0,0.0,1.0,1.0,0.0,1.0,-1.0,0.0 count:0
,-2.0,-2.0,0.0,-2.0,2.0,0.0,2.0,2.0,0.0,2.0,-2.0,0.0 count:1
,-3.0,-3.0,0.0,-3.0,3.0,0.0,3.0,3.0,0.0,3.0,-3.0,0.0 count:2
,-4.0,-4.0,0.0,-4.0,4.0,0.0,4.0,4.0,0.0,4.0,-4.0,0.0 count:3
,-5.0,-5.0,0.0,-5.0,5.0,0.0,5.0,5.0,0.0,5.0,-5.0,0.0 count:4
,-1.0,-1.0,0.0,-1.0,1.0,0.0,1.0,1.0,0.0,1.0,-1.0,0.0 count:5
・・・
◇ 解説
- アニメーションはAnimationTimerクラスを継承したMeshAnimationクラスにより実行している(88行目、97行目~154行目)。
- 一定間隔で呼ばれるhandle関数では0.5秒ごとにアニメーションカウントを進め(118行目~124行目)、メッシュに新しい頂点座標を設定している(127行目~149行目)
■ ボーン・アニメーション(スキニング、スケルタル・アニメーション)
ボーン・アニメーションとは、メッシュ内に架空の骨(及び関節)を設定し、骨の動きに連動してメッシュが変形するアニメーションである。メッシュ・アニメーションについては上で見てきたとおりなので、ここではボーンに連動してメッシュを動かす方法について確認する。
■ ボーン・アニメーションの基本
3Dレンダリングを行う場合、すべての3Dオブジェクトの位置はワールド座標系上の座標で表される。しかし、例えば人が歩く場合に『頭の位置は(x1,y1,z1)で、首の位置は(x2,y2,z2)で・・・』と設定していくのは直感的でない。そこで利用するのがボーン・アニメーションである。
ボーン・アニメーションは、関節の移動や角度を指定することにより対応するメッシュを動かす技法である。ボーン・アニメーションにより頂点座標を1つ1つ設定しなくてもよく、直感的なメッシュの操作が可能になる。ボーン・アニメーションにおいて、関節間には親子関係が存在する。例えば肩が動けば肘や手も動くように、親関節が動けば子関節が動くという関係である。
■ ボーン・アニメーションの用語整理
まずは、ボーン・アニメーションで利用する用語の整理を行う。
- ジョイント :架空の関節
- ボーン :架空の骨。ジョイントをつないだ線に相当
- バインディング・ポーズ:ボーンとメッシュを関連付ける時のポーズ
- ワールド座標系 :3Dオブジェクト位置を表す絶対座標系。1つだけ存在
- ローカル座標系 :ワールド座標系とは異なる中心もしくは軸を持つ座標系。無数に存在
- モデル座標系 :メッシュを宣言する際の頂点の座標系。ローカル座標系の一種
■ ジョイント(関節)のワールド座標計算
関節位置の計算で難しいことは、親の動きに連動して子も動くという動作である。この計算を楽にするため、すべての関節に親子関係を設定しルート関節を決めることからはじめる。
ワールド座標系における子関節の座標は『(ワールド座標系→親関節のローカル座標系)の座標変換+(親関節のローカル座標系→子関節のローカル座標系)の変換』と定義できる。下図の例で言うと、頂点2はローカル座標系1上の座標(x2,y2)であり、ワールド座標系→ローカル座標系1の座標変換を「平行移動(x1,y1)+回転θ」とする。このとき、頂点2をワールド座標系で表すと( x1+x2*cosθ , y1+y2*sinθ )となる。
親関節への座標変換が変化するたびに子のワールド座標系を再計算すれば、親に連動して子が動くという動作を再現できる。肩が回転する場合で考えると、『親の座標変換』が変化(回転)するたびに、所属している肘及びその子である手のワールド座標も再計算する。
■ メッシュののワールド座標計算
関節に連動してメッシュを動かすには、『関連付ける(バインドする)関節のローカル座標系の座標』でメッシュ座標を表現する必要がある。この座標を取得すると『関連付ける関節のローカル座標系への座標変換+メッシュ座標』でメッシュのワールド座標が計算できる。
しかし、3Dモデルファイルを見れば分かるように、メッシュの座標はメッシュ独自の座標系(モデル座標系)の座標で表現されている。このため、以下の順序で『連動するジョイントのローカル座標系』に変換する必要がある。
- モデル座標系でのジョイントの位置を宣言する
- メッシュ上の各頂点について、連動させるジョイントを設定(バインド)
- 連動させるジョイントのローカル座標系へ座標変換(座標を計算)する
1については、ジョイント位置を宣言する際にメッシュ座標系を利用すればよいだけである。関連付ける際のメッシュ及びジョイントの形をバインディング・ポーズと呼ぶ。2の頂点のバインドについては、頂点の宣言時に宣言する。3の座標変換はバインディング・ポーズにおいて、ジョイント位置を表現する座標変換の逆行列を適用することにより、ジョイントを中心とするローカル座標系に変換できる。
■ ボーン・アニメーションのサンプルコード
ボーン・アニメーションの実装を確認するサンプルを以下に示す。サンプルでは3つの四角形についてジョイント0~2を以下のように設定し、時間とともにジョイント1が回転する。下段の四角形はジョイント0にバインドし、上段の四角形はジョイント2にバインドしているため、ジョイント1の回転にあわせて中段の四角形が変形する。
◇サンプルコード
今回はサンプルプログラムが長くなったため、javaファイルをアップロードしている。
◇実行結果(実際は滑らかにアニメーションする)
◇解説
・メッシュの宣言
メッシュの宣言はTestBoneAnimation.java(82行目~130行目)で行っている。通常のメッシュと異なる点としては、以下の情報も宣言していることである。
- influenceCounts:i 番目の頂点に関連付ける関節の数。配列長は頂点数と等しい
- jointMatrixes :関節中心への座標変換。今回は3つの関節を宣言
- weights :関節への重み付け。今回は1頂点は1関節にしか依存しないため1.0に設定
- influences :i 番目の頂点に関連付ける関節と重みの組。i番目の頂点二ついてはinfuluenceCounts[i]の数だけ組を並べる
・アニメーションの設定
アニメーションの設定はTestBoneAnimation.java(138行目~183行目)で行っている。アニメーション設定では、一定間隔で関節1の角度を変化させ、SkinTriangleMesh::setJointMatrixes関数で関節を更新している。
・関節に連動したメッシュの移動
SkinTriangleMesh::setJointMatrixes関数で関節を更新されるたび、メッシュ位置の再計算を行っている(72行目~89行目)。SkinTriangleMesh::calcPoint関数がメッシュ位置の計算ロジックで、関連付けた関節のローカル座標への変換inverse(130行目)と、新しい関節位置の座標変換transform(136行目)を求め、重み付けによる加重平均を取って頂点位置を移動(161行目~163行目)している。
■ 参照
- JavaDoc - クラスTriangleMesh
- JavaDoc - インタフェースObservableFloatArray